# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: xguo

import hashlib
import hmac
import json
import logging
import re
import requests
import time

from urllib.parse import urlencode

from coin.base.timestamp import get_timestamp
from coin.exchange.base.kr_rest.auth_base import AuthKey
from coin.exchange.util.rate_limit import RateLimiter
from coin.exchange.base.kr_rest.rest_client_base import RestUpdate

_BITFLYER_API_URL_BASE = {
    'Japan': 'https://api.bitflyer.jp/v1/me/',
    'US': 'https://api.bitflyer.com/v1/me/',
    'Europe': 'https://api.bitflyer.com/v1/me/',
}


class AuthBase(object):
  def __init__(self, key_file, *, region='Japan', timeout=20, tps_limiter=None):
    key = AuthKey.from_file(key_file)
    self._access_key = key.access_key
    self._secret_key = key.secret_key
    self._api_url = _BITFLYER_API_URL_BASE[region]
    self._timeout = timeout
    self.tps_limiter = tps_limiter
    self.rate_limit = RateLimiter(200, 60)

  def get_auth_headers(self, url, method, params):
    assert method == 'GET' or method == 'POST'

    timestamp = str(int(round(time.time() * 1000000)))
    path = '/v1/me/' + url
    body = ''
    if method == 'POST':
      body = json.dumps(params)
    elif method == 'GET':
      if len(params) > 0:
        body = '?' + urlencode(params)
    text = timestamp + method + path + body
    sign = hmac.new(self._secret_key.encode('utf-8'), text.encode('utf-8'),
                    hashlib.sha256).hexdigest()

    headers = {
        'ACCESS-KEY': self._access_key,
        'ACCESS-TIMESTAMP': timestamp,
        'ACCESS-SIGN': sign,
    }
    if method == 'POST':
      headers['Content-Type'] = 'application/json'
    return headers

  def _query_nocheck(self, url, method, params=None):
    # Bitflyer treats None as invalid input. Remove from dict.
    if params:
      params = {k: v for k, v in params.items() if v is not None}

    headers = self.get_auth_headers(url, method, params)
    url = '/' + url if url[0] == '/' else url
    endpoint = self._api_url + url

    req_time = get_timestamp()
    if method == 'GET':
      response = requests.get(endpoint, headers=headers, params=params, timeout=self._timeout)
    elif method == 'POST':
      response = requests.post(endpoint,
                               headers=headers,
                               data=json.dumps(params),
                               timeout=self._timeout)
    else:
      raise ValueError('Invalid method.')
    res_time = get_timestamp()

    if response.status_code == requests.codes.ok:
      if response.text:
        response_json = response.json()
      else:
        response_json = None
    else:
      response_json = None
      logging.error("Wrong resp: %s\n%s", response.text, dict(response.headers))
      raise RuntimeError(response)

    # Non-OK responses will not be counted by the tps limiter.
    if self.tps_limiter is not None:
      self.tps_limiter.append_tps_queue(req_time)
      self.tps_limiter.update_tps_queue(req_time)

    return RestUpdate(req_time / 10.**9,
                      res_time / 10.**9,
                      response_json,
                      status=response.status_code)

  def _query(self, endpoint, method, params=None, disable_rate_limit=False):
    if not disable_rate_limit:
      while not self.rate_limit.check():
        time.sleep(0.1)
    else:
      self.rate_limit.check()
    params = params or {}
    return self._query_nocheck(endpoint, method, params=params)


class BitflyerNativePrivateClient(AuthBase):
  def __init__(self, key_file, *, region='Japan', timeout=20, tps_limiter=None):
    super().__init__(key_file, region=region, timeout=timeout, tps_limiter=tps_limiter)

  # Get API Key Permissions
  def query_api_key_permissions(self):
    url = 'getpermissions'
    return self._query(url, 'GET')

  # ###########
  #  Assets   #
  # ###########
  # Get Account Asset Balance
  def query_account_asset_balance(self):
    url = 'getbalance'
    return self._query(url, 'GET')

  # Get Margin Status
  def query_margin_status(self):
    url = 'getcollateral'
    return self._query(url, 'GET')

  # Get Margin Status Account
  # Obtain details of your collateral balances (multiple currencies supported)
  def query_margin_status_account(self):
    url = 'getcollateralaccounts'
    return self._query(url, 'GET')

  # ###########$#################
  #  Deposits and Withdrawals   #
  # ###########$#################
  # Get Bitcoin/Ethereum Deposit Addresses
  def query_deposite_addresses(self):
    # url = 'getaddresses'
    raise NotImplementedError()

  # Get Bitcoin/Ether Deposit History
  def query_deposit_history(self):
    # url = 'getcoinins'
    raise NotImplementedError()

  # Get Bitcoin/Ether Transaction History
  def query_transaction_history(self):
    # url = 'getcoinouts'
    raise NotImplementedError()

  # Get Summary of Bank Accounts
  def query_summary_of_bank_accounts(self):
    # url = 'getbankaccounts'
    raise NotImplementedError()

  # Get Cash Deposits
  def query_cach_deposits(self):
    # url = 'getdeposits'
    raise NotImplementedError()

  # Withdrawing Funds
  def query_withdrawing_funds(self):
    # url = 'withdraw'
    raise NotImplementedError()

  # Get Deposit Cancellation History
  def query_deposit_cancellation_history(self):
    # url = 'getwithdrawals'
    raise NotImplementedError()

  # ##############
  #   Trading    #
  # ##############
  # Send a New Order
  def query_submit_order(self,
                         product_code,
                         child_order_type,
                         side,
                         price,
                         size,
                         minute_to_expire,
                         time_in_force):
    assert child_order_type in ('LIMIT', 'MARKET')
    assert child_order_type != 'MARKET'  # do not support market
    assert side in ('BUY', 'SELL')
    assert time_in_force in ('GTC', 'IOC', 'FOK')

    if type(price) == str and price.isdigit():
      price = int(price)
    if type(price) == float:
      # Ignore JPY's sub-digits. Not exactly same price, but ignorable.
      price = int(price)
    assert type(price) == int and price > 0
    if type(size) == str and re.match('^\d+(\.\d+){0,1}$', size):
      assert float(size) > 0
    elif type(size) == int:
      assert size > 0
      size = float(size)
    elif type(size) == float:
      assert size > 0
    else:
      raise ValueError("illformatted size %s" % size)

    url = 'sendchildorder'
    params = {
        'product_code': product_code,
        'child_order_type': child_order_type,
        'side': side,
        'price': price,
        'size': size,
        'minute_to_expire': minute_to_expire,
        'time_in_force': time_in_force,
    }
    update = self._query(url, 'POST', params)
    return update

  # Send a Cancel Order
  def query_cancel_order(self, product_code, child_order_acceptance_id):
    url = 'cancelchildorder'
    params = {
        'product_code': product_code,
        'child_order_acceptance_id': child_order_acceptance_id,
    }
    update = self._query(url, 'POST', params)
    return update

  # Submit New Parent Order (Special order)
  def query_submit_parent_order(self):
    raise NotImplementedError()

  # Cancel parent order
  def query_cancel_parent_order(self):
    raise NotImplementedError()

  # Cancel all orders
  def query_cancel_product_orders(self, product_code):
    url = 'cancelallchildorders'
    params = {
        'product_code': product_code,
    }
    update = self._query(url, 'POST', params)
    return update

  # List orders
  def query_list_orders(self,
                        product_code,
                        child_order_acceptance_id,
                        child_order_state,
                        count,
                        before,
                        after):
    url = 'getchildorders'
    assert child_order_state in (None, 'ACTIVE', 'COMPLETED', 'CANCELED', 'EXPIRED', 'REJECTED')
    params = {
        'product_code': product_code,
        'child_order_acceptance_id': child_order_acceptance_id,
        'count': count,
        'before': before,
        'after': after,
    }
    if child_order_state is not None:
      params['child_order_state'] = child_order_state
    update = self._query(url, 'GET', params)
    return update

  # List parent orders
  def query_list_parent_orders(self):
    raise NotImplementedError()

  # Get Parent Order Details
  def query_parent_order_details(self):
    raise NotImplementedError()

  # List Executions
  def query_list_executions(self,
                            product_code,
                            count,
                            before,
                            after,
                            child_order_acceptance_id=None):
    url = 'getexecutions'
    params = {
        'product_code': product_code,
        'child_order_acceptance_id': child_order_acceptance_id,
        'count': count,
        'before': before,
        'after': after,
    }
    update = self._query(url, 'GET', params)
    return update

  # Get open interest summary
  def query_open_interest_summary(self, product_code):
    url = 'getpositions'
    params = {
        'product_code': product_code,
    }
    update = self._query(url, 'GET', params)
    return update

  # Get margin change history
  def query_margin_change_history(self, count, before, after):
    url = 'getcollateralhistory'
    params = {
        'count': count,
        'before': before,
        'after': after,
    }
    update = self._query(url, 'GET', params)
    return update

  # Get Trading commission
  def query_trading_commission(self, product_code):
    url = 'gettradingcommission'
    params = {
        'product_code': product_code,
    }
    update = self._query(url, 'GET', params)
    return update
